환경변수·시그널 실습 — getenv 복사·atexit와 sigaction·pselect 시그널 카운터

NOTE

C 기초 강의의 환경변수(11-01 ~ 11-03)와 시그널(12-01 ~ 12-04) 실습에서 만난 오류 패턴을 정리하고, 이를 바탕으로 로그 기록기(env_ex.c), PATH 탐색기(env_ex2.c), 시그널 카운터 두 개(sig_ex.c, sig_ex2.c)를 직접 만들며 겪은 버그를 기록한다.

환경은 Docker Desktop의 Linux 컨테이너(c_app_part1), gcc -Wall -Wextra.

IMPORTANT

코드는 컨테이너에서 컴파일·실행해 확인했고, 설명은 AI(Claude)와 함께 코드 리뷰하며 정리했다.

TIP

📂 원본 소스 (GitHub)

TIP

📂 환경

  • 접속: docker exec -it c_app_part1 /bin/bash
  • 시그널 보내기: kill -HUP <PID> (예: kill -HUP 1501), 종류는 HUP, USR1, USR2, INT, TERM
  • 컨테이너의 gcc가 오래돼서(4.8) -Wvla-parameter 같은 최신 경고 옵션은 쓸 수 없다.

📌 한눈에 보기

실습문제처방
11-01getenv("PATH")를 char path[32]에 strcpy → 환경변수 길이는 외부 값이라 넘침길이 검사 후 snprintf/strncpy + 널 종료, 또는 strlen+1만큼 malloc
11-02getenv 반환 문자열을 직접 바꾸려 함반환값은 읽기 전용으로 취급, 복사해서 수정
11-03atexit 등록 후 _Exit로 종료 → 핸들러가 실행되지 않음종료 경로는 return/exit, 핸들러는 등록의 역순으로 실행
12-01시그널 시점에 따라 일부가 무시됨(중복 시그널)sigaction + 시그널 마스크
12-02signal()은 시스템마다 동작이 다름(UNIX는 유지, Windows는 1회 후 기본값)이식성이 필요하면 sigaction
12-03핸들러 안에서 fputs/free 호출 → 락 때문에 교착 가능핸들러는 flag 설정만
12-04핸들러가 strcpy로 전역 버퍼를 건드림 → 해제 후·쓰는 도중 접근flag만 세우고 처리는 main에서

1. 환경변수 — 11-01 ~ 11-03

길이가 정해지지 않은 외부 값 (11-01)

요약

  • 문제: strcpy(path[32], getenv("PATH"))로 스택 오버플로 (11-01)
  • 원인: 환경변수는 외부 값이라 길이·내용을 믿을 수 없음
  • 처방: NULL 검사 → 길이 검사 후 snprintf/strncpy+\0, 길이를 모르면 malloc(strlen+1)

반환 문자열은 수정하지 않는다 (11-02)

요약

  • 문제: getenv 반환 문자열을 직접 수정
  • 원인: 반환 포인터는 환경 영역을 가리킴
  • 처방: 복사본을 만들어 수정 (strdup 등)

atexit (11-03)

요약

  • 문제: atexit 등록 후 _Exit로 끝내면 핸들러가 실행되지 않음 (11-03)
  • 원인: _Exit/abort는 atexit·시그널 핸들러를 실행하지 않고, stdio flush는 구현마다 다름
  • 처방: 종료 경로는 return/exit, 핸들러 안에서 exit() 금지. 핸들러는 등록의 역순으로 실행 (LIFO)
  • 결과: glibc 2.17에서 exit() 13바이트, abort() 13바이트, _Exit() 0바이트

2. 실습 1 — 로그 기록기 env_ex.c

요구사항: LOG_PATH 환경변수로 로그 파일을 정하고(없으면 default.log), 입력받은 줄을 기록한다. q/quit으로 종료하고, 종료 시 atexit로 등록한 clean_up()에서 Program terminated normally를 쓰고 파일을 닫는다.

처음 겪은 segfault

요약

  • 문제: open(O_CREAT|O_EXCL) → fdopen(fd, "w") 실패 → NULL fp로 fwrite해서 segfault
  • 원인: 접근 모드가 없으면 읽기 전용으로 열리고, fdopen 반환값을 검사하지 않음
  • 처방: O_WRONLY|O_CREAT|O_APPEND, fdopen이 NULL이면 perror+close(fd)

고친 점


3. 실습 2 — PATH 탐색기 env_ex2.c

요구사항: which와 비슷하게 PATH의 디렉토리를 순서대로 검색해 실행 가능한 파일의 전체 경로를 출력한다. -a면 일치하는 모든 경로를 출력하고, 종료 코드는 찾음 0 / 못 찾음 1 / 인자 오류·PATH 없음 2다.

getenv 문자열 수정 금지, 길이는 모두 동적 할당, 빈 항목은 현재 디렉토리, atexit 핸들러로 해제와 “검색한 디렉토리 개수” 출력.

처음 코드의 문제

요약

  • 문제: path_list가 NULL인 채 사용, getenv 결과에 strtok, 연속 구분자로 빈 항목 소실
  • 원인: 배열 미할당, 원본 문자열 수정, strtok이 연속 구분자를 건너뜀, 항목 수는 : 개수 + 1
  • 처방: 항목 수를 센 뒤 malloc, strdup 복사본을 strchr/strsep로 쪼개기

고친 구조

요약

  • 문제: getenv 수정 금지, 길이는 동적 할당, 빈 항목은 현재 디렉토리, 종료 시 해제와 검색 개수 출력
  • 처방: g_path_copy(복사본) + g_dirs(포인터 배열) + atexit 2개(해제는 마지막에 실행). stat+S_ISREG로 일반 파일 확인
  • 결과: ls → /usr/bin/ls(종료 0), 없으면 1, 사용법 오류 2

확인 결과


4. 시그널 — 12-01 ~ 12-04

시점에 따라 무시된다 (12-01)

요약

  • 문제: 핸들러 실행 중 같은 시그널이 겹치면 일부 시그널이 사라짐 (12-01)
  • 원인: 같은 번호의 시그널은 대기 중이면 하나로 합쳐짐
  • 처방: sigaction + sa_mask/sigprocmask로 끼어들 시그널 차단. 순서가 중요한 처리는 flag로

signal()의 이식성 (12-02)

요약

  • 문제: signal()은 구현마다 핸들러가 유지되거나 1회 후 기본 동작으로 복구됨 (12-02)
  • 원인: C 표준이 유지 여부를 정하지 않음
  • 처방: 유지/1회를 코드로 명시: sigaction(SA_RESETHAND 여부). signal 안 signal() 재등록은 경합 구간 존재

핸들러 안에서 하면 안 되는 것 (12-03, 12-04)

요약

  • 문제: 핸들러 안의 fputs/free/strcpy로 교착 또는 해제된 메모리 접근 (12-03, 12-04)
  • 원인: 핸들러는 어느 지점에서든 끼어들고, printf·malloc 계열은 async-signal-safe가 아님
  • 처방: volatile sig_atomic_t flag만 세우고 처리는 main에서. 로그가 필요하면 write(2, ...)

5. 실습 3 — 1초 카운터 sig_ex.c

요구사항: 무한 루프에서 count를 올리고 1초 뒤 파일에 저장한다. SIGINT는 무시(메시지만), SIGTERM에만 종료한다.

v1의 문제

요약

  • 문제: busy loop, int 오버플로, fd == 0 검사, 핸들러 안 printf, fdopen 실패 시 close 누락
  • 원인: 1초 대기와 open 실패 값(-1)을 잘못 처리
  • 처방: 1초 대기 후 증가, unsigned long, fd < 0, write, sigaction, 1초마다 저장

v2의 핵심 — sleep이 시그널에 깨어나는 문제

요약

  • 문제: sleep/nanosleep이 시그널로 일찍 깨어나 카운터가 빨라짐
  • 원인: SIGINT를 무시해도 대기 시간은 줄어듦
  • 처방: EINTR이면 rem으로 남은 시간만큼 다시 대기, SIGTERM이면 중단. SA_RESTART 미사용
  • 결과: SIGINT 2번에도 생존, 5.3초 시점에 count.txt가 5

6. 실습 4 — 시그널로 운영되는 카운터 sig_ex2.c

요구사항: 1초마다 카운터를 interval씩 증가.

SIGHUP은 설정파일(12-07.conf)에서 interval을 다시 읽고, SIGUSR1은 카운터·interval·시그널별 처리 횟수를 출력, SIGUSR2는 카운터를 0으로, SIGTERM은 최종값을 출력하고 종료, SIGINT는 무시한다. 주의: sigaction과 시그널 마스크 사용, 핸들러는 flag만, sleep 중 시그널이 오면 바로 처리하되 카운터는 1초에 한 번만, 설정파일이 없거나 값이 잘못되면 에러 메시지를 출력하고 기존 interval 유지.

첫 구현의 문제

v2 — 마스크와 pselect로 “즉시 반응, 증가는 1초에 한 번”

요약

  • 문제: sleep(1)이 시그널마다 깨어 카운터가 올라가고, flag 확인 직후 온 시그널은 1초 뒤에 처리
  • 원인: 깨어난 이유(시각 도달/시그널)를 구분하지 않고 flag 확인과 sleep 사이에 틈이 있음
  • 처방: 평소엔 sigprocmask로 차단, 대기 중에만 pselect로 해제(해제와 대기가 원자적). 다음 증가 시각을 CLOCK_MONOTONIC 절대 시각으로 관리
  • 결과: USR1 5번 즉시 출력, INT 10번에도 카운터 정상, SIGTERM 약 14ms에 종료
flowchart TD
    A["sigprocmask(SIG_BLOCK)<br/>평소에는 시그널 차단"] --> B["flag 처리<br/>block 구간이라 안전"]
    B --> T{"SIGTERM flag?"}
    T -- "예" --> Z(["final counter 출력 후 종료"])
    T -- "아니오" --> P["pselect(next, orig_mask)<br/>마스크 해제 + 대기를 한 번에"]
    P --> R{"깨어난 이유"}
    R -- "시각 도달" --> I["counter += interval<br/>next += 1초"]
    R -- "시그널" --> K["next 그대로 유지"]
    I --> B
    K --> B

✅ 정리 — 환경변수·시그널 점검 체크리스트